7-1 策略权限控制总结
策略权限控制的整体回顾
策略权限(Policy-Based Access Control)在 RBAC 的基础上提供了更细粒度的控制能力。RBAC 只能判断"用户能否访问某个接口",而策略权限可以判断"用户能否对某个资源的某个实例执行某个操作"——比如用户只能编辑自己创建的文章,管理员可以编辑所有文章。
整个策略权限模块涉及数据库设计、CASL Ability 集成、Policy Guard 核心逻辑等多个层面。
CASL Ability 的三种模式
在 casl-ability.service.ts 中,策略权限通过 @casl/ability 库实现了三种条件判断模式:
| 模式 | 创建方式 | 适用场景 |
|---|---|---|
| JSON 条件 | createMongoAbility() | 简单的键值对条件,如 { authorId: user.id } |
| MongoDB 查询 | createMongoAbility() + buildMongoQueryMatcher() | 复杂查询条件,支持 $ne、$in、$gt 等操作符 |
| 函数式条件 | createPureAbility() + PureAbility | 需要运行时计算的动态条件 |
JSON 模式与 MongoDB 模式
JSON 和 MongoDB 模式都使用 createMongoAbility() 工厂方法创建 Ability 实例。MongoDB 模式的关键在于 buildMongoQueryMatcher:
// src/casl/casl-ability.service.ts
import {
createMongoAbility,
buildMongoQueryMatcher,
MongoQuery,
AbilityBuilder,
} from '@casl/ability';
import { allPermissionsRead, allPermissionsInterpreters } from '@ucast/mongo2js';
// 创建 MongoDB 查询匹配器
// 将 MongoDB 查询操作符与 ucast 的本地方法建立关联
const conditionsMatcher = buildMongoQueryMatcher(
...allPermissionsRead,
...allPermissionsInterpreters,
);
export function createAbilityForUser(rules: any[]) {
const { ability } = new AbilityBuilder(createMongoAbility);
return ability;
}
typescript
buildMongoQueryMatcher 的作用是将 MongoDB 查询操作符(如 $ne、$in、$or)映射到 ucast 库的本地方法,从而支持类似 MongoDB 的查询语法来编写对象条件。
函数式模式
函数式模式使用 createPureAbility 创建实例,适合需要在运行时进行复杂逻辑判断的场景:
import { createPureAbility, PureAbility } from '@casl/ability';
const ability = createPureAbility<[string, string]>([
{ action: 'update', subject: 'Article', conditions: { authorId: '{{user.id}}' } },
]);
typescript
Policy Guard 的核心逻辑
Policy Guard 是整个策略权限模块的核心,负责在请求到达 Controller 之前判断用户是否拥有访问权限。
执行流程
1. 接口侧:通过 @Require() 装饰器获取该接口所需的 permission
↓
2. 根据 permission 查询关联的 Policy 列表(一个接口可关联多个 Policy)
↓
3. 用户侧:根据用户的角色查询该角色拥有的所有 Policy
↓
4. 将用户的 Policy 转换为 Ability 实例数组
↓
5. 双重循环判断:
- 外层循环:接口所需的 Policy 列表
- 内层循环:用户拥有的 Ability 实例
↓
6. 使用 ability.can() 方法判断是否匹配
- 匹配成功 → 从待验证列表中移除该 Policy
- 匹配失败 → 继续尝试下一个 Ability
↓
7. 最终判断:
- 待验证列表为空 → 全部通过 → 放行请求
- 待验证列表非空 → 存在未通过的 Policy → 返回 403
text
核心代码结构
// src/policy/policy.guard.ts
@Injectable()
export class PolicyGuard implements CanActivate {
constructor(
private reflector: Reflector,
private policyService: PolicyService,
private caslAbilityService: CaslAbilityService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. 获取接口所需的权限
const permission = this.reflector.get('permission', context.getHandler());
if (!permission) return true;
// 2. 查询接口关联的 Policy 列表
const policyEndpoints = await this.policyService.findByPermission(permission);
// 3. 获取用户信息(含 roles、permissions、policies)
const request = context.switchToHttp().getRequest();
const user = request.user;
// 4. 根据用户角色构建 Ability 实例数组
const abilities = this.caslAbilityService.buildAbilities(user.policies);
// 5. 双重循环判断
const unmatchedPolicies = policyEndpoints.filter(policy => {
return !abilities.some(ability => {
// 如果有 fields 限制,使用 can 方法的 fields 参数
if (policy.fields && policy.fields.length > 0) {
return ability.can(policy.action, policy.subject, policy.fields);
}
return ability.can(policy.action, policy.subject);
});
});
// 6. 如果所有 Policy 都已匹配,则放行
return unmatchedPolicies.length === 0;
}
}
typescript
Subject 映射的重要性
在使用 ability.can() 方法时,CASL 需要知道 subject 字符串与实际 Entity 类之间的对应关系。这是因为 can 方法在判断字段级权限时,需要通过 Entity 类来获取字段定义。
// Subject 映射应该定义在模块内部
const subjectMap = {
Article: Article,
Comment: Comment,
Post: Post,
};
// 使用 class-transformer 将数据库数据与 Entity 关联
const instance = plainToInstance(subjectMap[policy.subject], dbData);
ability.can(policy.action, instance);
typescript
注意:这个 subjectMap 的映射必须在使用 can 方法之前定义好,不能临时创建,否则字段级权限判断会失效。
数据库层面的强化
策略权限模块同时强化了 RBAC 数据库层面的增删改查逻辑:
- 角色管理:角色的 CRUD 操作,支持分页查询
- 权限管理:权限与角色的关联管理
- 策略管理:Policy 的创建、更新、删除,支持条件字段
- 接口权限映射:接口与 Policy 的关联关系
这些基础操作在 Prisma 中实现后,MongoDB 和 TypeORM 的版本可以借助 AI 工具来完成迁移。
实践建议
| 建议 | 说明 |
|---|---|
| 动手调试 | 策略权限逻辑复杂,光看代码很难理解,建议断点调试 |
| 先理解再实现 | 先注释掉老师写的代码,自己尝试实现,卡住时再参考 |
| 加入白名单 | 对于安全级别较高的模块,将 Policy Guard 加入其中 |
| 注意 subject 映射 | can 方法需要 subject 与 Entity 类的映射关系 |
| 测试用例覆盖 | 策略权限涉及多种条件组合,需要充分的测试覆盖 |
策略权限的引入让整个权限系统从"接口级控制"升级到"资源实例级控制",是 RBAC 模型的重要补充。虽然实现复杂度较高,但在需要精细化权限管理的业务场景中(如多租户系统、内容管理平台),这一层控制是不可或缺的。
↑